/* Compatability with old browsers*/

if (Object.defineProperties === undefined) {
	Object.defineProperties = function(obj, properties) {
		  function convertToDescriptor(desc)
		  {
		    function hasProperty(obj, prop)
		    {
		      return Object.prototype.hasOwnProperty.call(obj, prop);
	
		    function isCallable(v)
		    {
		      // NB: modify as necessary if other values than functions are callable.
		      return typeof v === "function";
		    }
	
		    if (typeof desc !== "object" || desc === null)
		      throw new TypeError("bad desc");
	
		    var d = {};
		    if (hasProperty(desc, "enumerable"))
		      d.enumerable = !!obj.enumerable;
		    if (hasProperty(desc, "configurable"))
		      d.configurable = !!obj.configurable;
		    if (hasProperty(desc, "value"))
		      d.value = obj.value;
		    if (hasProperty(desc, "writable"))
		      d.writable = !!desc.writable;
		    if (hasProperty(desc, "get"))
		    {
		      var g = desc.get;
		      if (!isCallable(g) && g !== "undefined")
			throw new TypeError("bad get");
		      d.get = g;
		    }
		    if (hasProperty(desc, "set"))
		    {
		      var s = desc.set;
		      if (!isCallable(s) && s !== "undefined")
			throw new TypeError("bad set");
		      d.set = s;
		    }
	
		    if (("get" in d || "set" in d) && ("value" in d || "writable" in d))
		      throw new TypeError("identity-confused descriptor");
	
		    return d;
		  }
	
		  if (typeof obj !== "object" || obj === null)
		    throw new TypeError("bad obj");
	
		  properties = Object(properties);
		  var keys = Object.keys(properties);
		  var descs = [];
		  for (var i = 0; i < keys.length; i++)
		    descs.push([keys[i], convertToDescriptor(properties[keys[i]])]);
		  for (var i = 0; i < descs.length; i++)
		    Object.defineProperty(obj, descs[i][0], descs[i][1]);
	
		  return obj;
		}
	}
}

window.Tiggr = (function() {

	var components = {};
	
	var tiggrFunction = function(id) {
		return components[id];
	};
	
	tiggrFunction.__registerComponent = function(id, component) {
		components[id] = component;
	};
	
	tiggrFunction.__unregisterComponent = function(id) {
		delete components[id];
	};

	return tiggrFunction;
}());

(function($t, $) {

	var linkPrototypes = function (parent, child) {
		var F = function() {};
		F.prototype = parent.prototype;
		child.prototype = new F();
	    child.prototype.constructor = child;
	    child.$super = parent.prototype;

	    return child;
	};	

	$t.createClass = function(base, methods, properties)  {
		base = base || Object;
		methods = methods || {};
		properties = properties || {};
		
		var derived = function()  {
			if (this.init) {
				this.init.apply(this, $.makeArray(arguments));
			}
		};
		linkPrototypes(base, derived);
	    $.extend(derived.prototype, methods);

    	Object.defineProperties(derived.prototype, properties);
	    
	    return derived;
	};
	
	$t.BaseComponent = $t.createClass(null, {

		init: function(options) {
			this.options = options;
			this.__element = $('[dsid="' + options.id + '"]:eq(0)', options.context);
			
			this.attachToDOM();
		},
		
		__registerComponent: function() {
			this.__componentRegistered = true;
			$t.__registerComponent(this.id, this);
			return this;
		},

		destroy: function() {
			if (this.__componentRegistered) {
				$t.__unregisterComponent(this.id);
			}
			this.__element = $();
		},
		
		__getUnknownAttribute: function(name) {
			return this.element().attr(name);
		},
		
		__getAttribute: function(name) {
			if (this.hasOwnProperty(name)) {
				return this[name];
			} else {
				return this.__getUnknownAttribute(name);
			}
		},
		
		__setUnknownAttribute: function(name, value) {
			this.element().attr(name, value);
		},
		
		__setAttribute: function(name, value) {
			if (this.hasOwnProperty(name)) {
				this[name] = value;
			} else {
				this.__setUnknownAttribute(name, value);
			}
		},

		getId: function() {
			return this.id;
		},
		
		element: function() {
			return this.__element;
		},
		
		attachToDOM: function() {
			this.element().data('Tiggr.component', this);
		},
		
		attr: function() {
			if (arguments.length == 1) {
				return this.__getAttribute(arguments[0]);
			} else {
				this.__setAttribute(arguments[0], arguments[1]);
				return this;
			}
		}
	
	}, {
		id: {
			get: function() {
				return this.options.id;
			},
			configurable : false
		}
	});
	
	var eventHandlerDelegates = ['bind', 'unbind', 'one', 'live', 'die', 'trigger'];
	$.each(eventHandlerDelegates, function(idx, methodName) {
		$t.BaseComponent.prototype[methodName] = function() {
			var elts = this.element();
			return elts[methodName].apply(elts, $.makeArray(arguments));
		};
	});	

	var commonEvents = ['mouseup', 'mousedown', 'mouseover', 'mouseout', 'mousemove', 'keypress', 'keyup', 'keydown', 'dblclick', 'click'];
	$.each(commonEvents, function(idx, type) {
		$t.BaseComponent.prototype[type] = function() {
			if (arguments.length == 0) {
				return this.trigger(type);
			} else {
				return this.bind.apply(this, $.makeArray(arguments));
			}
		};
	});

	$t.ConsoleLogger = $t.createClass(null, {

		__formatMessage: function(message, params) {
			var params = $.makeArray(arguments);
			params.shift();
			
			return message.replace(/\{(\d+)\}/g, function(str, p1, offset, s) {
				var num = parseInt(p1, 10);
				return params[num - 1 /* one-based -> zero-based */];
			});
		},
		
		error: function(message, params) {
			if (this.isLevelEnabled($t.Logger.Levels.ERROR)) {
				console.error(this.__formatMessage(message, params));
			}
		},
		
		warn: function(message, params) {
			if (this.isLevelEnabled($t.Logger.Levels.WARN)) {
				console.warn(this.__formatMessage(message, params));
			}
		},

		info: function(message, params) {
			if (this.isLevelEnabled($t.Logger.Levels.INFO)) {
				console.info(this.__formatMessage(message, params));
			}
		},

		debug: function(message, params) {
			if (this.isLevelEnabled($t.Logger.Levels.DEBUG)) {
				console.log(this.__formatMessage(message, params));
			}
		},
		
		clear: function() {
			console.clear();
		},
		
		dir: function(o) {
			console.dir(o);
		},
		
		setLevel: function(level) {
			this.level = level;
		},
		
		getLevel: function() {
			return this.level || $t.Logger.Levels.DEBUG;
		},
		
		isLevelEnabled: function(level) {
			return this.getLevel() <= level;
		}
	});
	
	var booleanAttributes = {
		'checked': true, 'compact': true, 'declare': true, 'defer': true, 'disabled': true, 'ismap': true, 
		'multiple': true, 'nohref': true, 'noresize': true, 'noshade': true, 'nowrap': true, 'readonly': true, 
		'selected': true
	};
	
	var requestDefaults = {
		dataType: 'json',
		cache: true,
		crossDomain: true,
		timeout: 50000
	};

	$t.MappingVisitor = $t.createClass(null, {
		
		init: function() {
			this.__contexts = new Array();
			this.__mappings = new Array();
			this.__visitArrayElement$Proxied = $.proxy(this.__visitArrayElement, this);
		},
		
		findComponent: function() {
			var context = this.getContext();
			
			var name = this.getMapping().ID;
			
			if (name) {
				return context.find("[dsid='" + name + "'], [dsrefid='" + name + "']");
			} else {
				return context;
			}
		},

		pushPath: function(p) {
		},
		
		popPath: function() {
		},
		
		getContext: function() {
			return this.__contexts[this.__contexts.length - 1];
		},
		
		getMapping: function() {
			return this.__mappings[this.__mappings.length - 1];
		},
		
		__validateRequiredMappingAttributes: function(entry) {
			//if (!entry.PATH) {
			//	$t.log.error("Error processing entry {0} - no PATH specified", JSON.stringify(entry));
			//	return false;
			//}
			
			return true;
		},

		beforeArrayElementVisit: function(elt, idx) {
		},
		
		afterArrayElementVisit: function(elt, idx) {
		},
		
		beforeArrayVisit: function() {
		},
		
		afterArrayVisit: function() {
		},
		
		visitEntry: function() {
		},
		
		getArrayElements: function() {
			return [];
		},
		
		__visitArrayElement: function(idx, elt) {
			this.pushPath(idx);
			
			this.beforeArrayElementVisit(elt, idx);
			
			this.__visitMappingsArray(this.getMapping().SET);
			
			this.afterArrayElementVisit(elt, idx, this.getMapping());

			this.popPath();
		},
		
		__visitMapping: function(mapping) {
			if (!this.__validateRequiredMappingAttributes(mapping)) {
				return;
			}
			
			if (mapping.PATH) {
				for (var i = 0; i < mapping.PATH.length; i++) {
					this.pushPath(mapping.PATH[i]);
				}
			}

			this.__mappings.push(mapping);
			
			if (!mapping.SET) {
				this.visitEntry();
			} else {
				this.beforeArrayVisit();

				$.each(this.getArrayElements(), this.__visitArrayElement$Proxied);

				this.afterArrayVisit();
			}
			
			this.__mappings.pop();

			if (mapping.PATH) {
				for (var i = 0; i < mapping.PATH.length; i++) {
					this.popPath();
				}
			}
		},
		
		__visitMappingsArray: function(mappings) {
			for (var i = 0; i < mappings.length; i++) {
				this.__visitMapping(mappings[i]);
			}
		},
		
		visit: function(mappings) {
			this.__contexts.push($(document));

			this.__visitMappingsArray(mappings);
			
			this.__contexts.pop();
		}
	});
	
	$t.RenderVisitor = $t.createClass($t.MappingVisitor, {
		init: function(data) {
			$t.RenderVisitor.$super.init.apply(this);
			this.data = data;
			this.__path = new Array();
			this.__components = new Array();
		},
		
		__setupDisplay: function(cs) {
			cs.each(function(i) {
				var c = cs.eq(i);
				
				var display = c.prop('__tiggrDisplay');
				if (typeof display == 'undefined') {
					//first time call - backup 'display' settings
					c.prop('__tiggrDisplay', c.css('display'));
				} else {
					c.css('display', display);
				}
			});
		},
		
		pushPath: function(p) {
			this.__path.push(p);
		},
		
		popPath: function() {
			this.__path.pop();
		},
		
		__getDataByPath: function() {
			var path = this.__path.join('.');
			return jsonPath(this.data, path)[0];
		},

		__getComponents: function() {
			return this.__components[this.__components.length - 1];
		},
		
		getArrayElements: function() {
			return this.__getDataByPath();
		},
		
		beforeArrayElementVisit: function(elt, idx) {
			var clonedElements = new Array();

			var components = this.__getComponents();
			components.each(function(componentIdx) {
				var component = components.eq(componentIdx);
				
				var dsid = component.attr('dsid');
				var clonedComponent;
				
				if (component.attr('data-role') === 'collapsible') {
					clonedComponent = $('<div data-role="collapsible"><h3></h3><div></div></div>');
					clonedComponent.appendTo(component.parent()).collapsible();
					clonedComponent.children('h3').html(component.children('h3').html());
					clonedComponent.children('div').html(component.children('div').html());
					for(var i = 0;  i < component[0].attributes.length; i++)  {
						clonedComponent[0].attributes.setNamedItem(component[0].attributes.item(i).cloneNode(true));
					}
					if(component.children('h3').hasClass('ui-collapsible-heading-collapsed'))
						clonedComponent.children('h3').click();					
				} else {
					clonedComponent = component.clone().appendTo(component.parent());
				}
				$.merge(clonedElements, clonedComponent);
				
				clonedComponent.attr('dsid', dsid + idx).attr('dsrefid', dsid);
			});
			
			this.__contexts.push($(clonedElements));
		},
		
		beforeArrayVisit: function() {
			this.findComponent().filter('[dsrefid]').remove();
			var components = this.findComponent();
			this.__setupDisplay(components);
			this.__components.push(components);
		},
		
		afterArrayElementVisit: function(elt, idx, mappingSet) {
			var el = this.__contexts.pop();
			if(mappingSet && mappingSet.TRANSFORMATION)  {
				mappingSet.TRANSFORMATION(elt, el);
			}			
		},
		
		afterArrayVisit: function() {
			var components = this.__components.pop();
			components.hide();
		},
		
		__updateComponent: function() {
			var entry = this.getMapping();
			var value = this.__getDataByPath() || '';
			var elt = this.findComponent();

			if(entry.TRANSFORMATION)  {
				var result = entry.TRANSFORMATION(value, elt);
				if(result) value = result; 
			}
			
			//TODO remove this legacy case (attrib!='@')
			if (entry.ATTR && entry.ATTR != '@') {
				//TODO - boolean value conversion
				//attribute can have boolean/integer type so we don't do string conversion. or do we need?
		
				if (booleanAttributes[entry.ATTR] && typeof value != 'boolean') {
					value = /^\s*true\s*$/i.test(String(value));
				}
				
				elt.attr(entry.ATTR, value);
			} else {
				//TODO - do we need explicit string constructor call here?
				//TODO - this will erase all child elements, should we handle this somehow?
				elt.text(String(value));
			}
		},
		
		visitEntry: function() {
			var entry = this.getMapping();
			var value = this.__getDataByPath() || '';
			
			if (entry.ID == '___local_storage___') {

				if(entry.TRANSFORMATION)  {
					var result = entry.TRANSFORMATION(value, undefined);
					if(result) value = result;
				};
				
				if (typeof value != 'undefined') {
					$t.persistentStorage.setItem(entry.ATTR, value);
				} else {
					$t.persistentStorage.removeItem(entry.ATTR);
				};
			} else  if(entry.ID == undefined && entry.TRANSFORMATION) {
				var element = this.findComponent();
				entry.TRANSFORMATION(value, element);
			} else {
				this.__updateComponent();
			}
		}
	});
	
	$t.RequestDataBuilderVisitor = $t.createClass($t.MappingVisitor, {
		init: function(isJsonp) {
			$t.RequestDataBuilderVisitor.$super.init.apply(this);
			this.__params = {};
			this.__headers = {};
			
			this.__paramStack = new Array();
			this.__paramStack.push(this.__params);

			this.__headerStack = new Array();
			this.__headerStack.push(this.__headers);

			this.__paths = new Array();
			
			this.__currentPath = new Array();
			
			this.__isJsonp = isJsonp;
		},

		__getParam: function() {
			return this.__paramStack[this.__paramStack.length - 1];
		},

		__getHeader: function() {
			return this.__headerStack[this.__headerStack.length - 1];
		},

		__clearPath: function() {
			this.__currentPath = new Array();
		},
		
		pushPath: function(p) {
			this.__currentPath.push(p);
		},
		
		popPath: function() {
			if (this.__currentPath.length != 0) {
				this.__currentPath.pop();
			}
		},
		
		__setValue: function(data, value) {
			for (var i = 0; i < this.__currentPath.length - 1; i++) {
				var p = this.__currentPath[i];
				if (!data[p]) {
					data = data[p] = {};
				} else {
					data = data[p];
				}
			}
			
			var pathSegment = this.__currentPath[this.__currentPath.length - 1];

			var prevValue = data[pathSegment];
			if (typeof prevValue != 'undefined') {
				if ($.isArray(prevValue)) {
					prevValue.push(value);
				} else {
					data[pathSegment] = [data[pathSegment], value];
				}
			} else {
				data[pathSegment] = value;
			}
		},
		
		__isHeader: function() {
			var l = this.__mappings.length - 1;
			for (var i = l; 0 <= i && l - 2 < i; i--) {
				var mapping = this.__mappings[i];
				if (mapping.HEADER) {
					return true;
				}
			}
			
			return false;
		},
		
		findComponent: function() {
			var elts = $t.RequestDataBuilderVisitor.$super.findComponent.call(this);
			var clonedElts = elts.filter('[dsrefid]');

			return (clonedElts.length ? clonedElts : elts);
		},
		
		getArrayElements: function() {
			return this.findComponent();
		},

		beforeArrayElementVisit: function(elt, idx) {
			this.__contexts.push($(elt));
		},
		
		afterArrayElementVisit: function(elt, idx) {
			this.__contexts.pop();
		},
		
		beforeArrayVisit: function() {
			this.__paths.push($.merge([], this.__currentPath));
			this.__clearPath();

			this.__paramStack.push([]);
			this.__headerStack.push([]);
		},
		
		afterArrayVisit: function() {
			this.__currentPath = this.__paths.pop();

			var paramArray = this.__paramStack.pop();
			if (paramArray.length != 0) {
				this.__setValue(this.__getParam(), paramArray)
			}
			
			var headerArray = this.__headerStack.pop();
			if (headerArray.length != 0) {
				this.__setValue(this.__getHeader(), headerArray)
			}
			
			this.__clearPath();
		},
		
		__setHeaderOrValue: function(value) {
			this.__setValue(this.__isHeader() ? this.__getHeader() : this.__getParam(), value);
		},
		
		visitEntry: function() {
			var entry = this.getMapping();
			var result;
			
			if (entry.ATTR) {
				var value;
				if (entry.ID == '___local_storage___') {
					value = $t.persistentStorage.getItem(entry.ATTR) || entry.ATTR;
					if(entry.TRANSFORMATION) {
						result = entry.TRANSFORMATION(value);
						if(result) value = result;
					}
					this.__setHeaderOrValue(value);
				} else if (this.__isJsonp && entry.ATTR == '?') {
					this.jsonp = this.__currentPath.join('.');
				} else {
					value = entry.ATTR;
					if(entry.TRANSFORMATION) {
						result = entry.TRANSFORMATION(value);
						if(result) value = result;
					}
					this.__setHeaderOrValue(value);
				}
			} else 	if (entry.ID) {
				var requestData = this.findComponent().serializeArray();
				if(entry.TRANSFORMATION) {
					result = entry.TRANSFORMATION(requestData[0].value);
					if(result) requestData[0].value = result;
				}
				for (var i = 0; i < requestData.length; i++) {
					this.__setHeaderOrValue(requestData[i].value);
				}
			} else if(entry.TRANSFORMATION) {
				result = entry.TRANSFORMATION(undefined);
				if(result) this.__setHeaderOrValue(result);
			}
		},
		
		getHeaders: function() {
			return this.__headers;
		},
		
		getParams: function() {
			return this.__params;
		},
		
		getJsonp: function() {
			return this.jsonp;
		}
	});
		
	$t.DataSource = $t.createClass(null, {
		
		init: function(options, requestOptions) {
			this.options = options;
			this.__outMappings = options.outMappings || [];
			this.__inMappings = options.inMappings || [];
			
			this.__requestOptions = $.extend(requestDefaults, requestOptions);
		},
		
		__setupDisplay: function(cs) {
			cs.each(function(i) {
				var c = cs.eq(i);
				
				var display = c.prop('__tiggrDisplay');
				if (typeof display == 'undefined') {
					//first time call - backup 'display' settings
					c.prop('__tiggrDisplay', c.css('display'));
				} else {
					c.css('display', display);
				}
			});
		},
		
		updateComponents: function(data) {
			this.data = data;
			
			new $t.RenderVisitor(data).visit(this.__outMappings);
		},
		
		__successHandler: function(data) {
			if(this.__requestOptions.dataType=="xml"){				
				this.updateComponents($.xml2json(data));
			}
			else{
				this.updateComponents(data);
			}
			
			if (this.options.onSuccess) {
				this.options.onSuccess.apply(this, $.merge([], arguments));
			}
		},
		
		__responseDataHandler: function() {
			var args = $.merge([], arguments);
			var data = args[0];		
			
			if ($.type(data) == 'string') {
				data = $.parseJSON(data);
				args[0] = data;
			}
			
			var transformer = this.getTransformer();
			if (transformer) {
				args[0] = transformer.transform(args[0]);
			}
			
			this.__successHandler.apply(this, args);
		},
		
		__errorHandler: function() {
			//we are not resetting this.data here - that's done intentionally
			
			//TODO store error data
			if (this.options.onError) {
				this.options.onError.apply(this, $.merge([], arguments));
			}
		},

		__completeHandler: function() {

			if (this.options.onComplete) {
				this.options.onComplete.apply(this, $.merge([], arguments));
			}
			hideSpinner();
		},

		__beforeSendHandler: function() {
			showSpinner();
		},
		
		__replaceUrlParams: function(settings) {
			if (settings.url) {
				settings.url = settings.url.replace(/\{(\w+)\}/g, function(str, param) {
					var paramValue = settings.data[param] || '';
					delete settings.data[param];
					
					if ($.isArray(paramValue)) {
						paramValue = paramValue.join(',');
					}
					
					return encodeURIComponent(paramValue);
				});
			}
		},
		
		__buildRequestSettings: function(settings) {
			var handlers = {
				'success':  $.proxy(this.__responseDataHandler, this),
				'error': $.proxy(this.__errorHandler, this),
				'complete':$.proxy(this.__completeHandler, this),
				'beforeSend':$.proxy(this.__beforeSendHandler, this)
			};
			settings = $.extend(handlers, this.__requestOptions, settings || {});
			
			var builder = new $t.RequestDataBuilderVisitor(!settings.jsonp && /\bjsonp\b/.test(settings.dataType || ''));
			builder.visit(this.__inMappings);
			
			settings.data = $.extend(builder.getParams(), settings.data || {});
			settings.headers = $.extend(builder.getHeaders(), settings.headers || {});
			
			this.__replaceUrlParams(settings);

			if (settings.contentType && settings.contentType.indexOf('json') !== -1) {
					settings.data  = JSON.stringify(settings.data);
			}

			var jsonp = builder.getJsonp();
			if (jsonp) {
				settings.jsonp = jsonp;
			}
			
				
			return settings;
		},
		
		execute: function(settings) {
			settings = this.__buildRequestSettings(settings);
			
			if (this.options.echo) {
				this.__successHandler(this.options.echo);
			} else if (this.options.securityContext) {
				//TODO - rework this
				this.options.securityContext.invoke({
					execute: $.ajax
				}, settings);
			} else {
				$.ajax(settings);
			}				
		},
		
		getTransformer: function() {
			return this.__transformer;
		},
		
		setTransformer: function(transformer) {
			this.__transformer = transformer;
		}
	});
	
	$t.screenStorage = new $t.createClass(null, $t.Storage);
	$t.persistentStorage = localStorage;
	$t.applicationStorage = sessionStorage;

} (Tiggr, jQuery));
